/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.fpl.gamecontroller; import android.content.Context; import android.hardware.input.InputManager; import android.hardware.input.InputManager.InputDeviceListener; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView.Renderer; import android.opengl.Matrix; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; import com.google.fpl.gamecontroller.particles.BackgroundParticleSystem; import com.google.fpl.gamecontroller.particles.ParticleSystem; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** * A singleton class that manages the OpenGL context, camera, players, etc. */ public class GameState extends GLSurfaceView implements Renderer, InputDeviceListener { // The "world" is everything that is visible on the screen. The world extends to the outside // edges of the walls that surround the world. // // These dimensions are somewhat arbitrary, and were chosen because they match the 16:9 // aspect ratio of many tablets and TVs. The world dimensions do not map directly to pixels // on the screen. During rendering, the world is scaled so that it takes up as much of the // screen as possible, but still maintains the 16:9 aspect ratio. For displays with different // aspect ratios, some portions of the display will be empty. public static final int WORLD_WIDTH = 1280 / 4; public static final int WORLD_HEIGHT = 720 / 4; // Define the rectangle that bounds the world. The coordinate system used by the world // is centered around 0, 0. public static final int WORLD_TOP_COORDINATE = WORLD_HEIGHT / 2; public static final int WORLD_BOTTOM_COORDINATE = -WORLD_HEIGHT / 2; public static final int WORLD_LEFT_COORDINATE = -WORLD_WIDTH / 2; public static final int WORLD_RIGHT_COORDINATE = WORLD_WIDTH / 2; // Player's ships are identified with positive integers. This value indicates an invalid // or unassigned player. public static final int INVALID_PLAYER_ID = -1; // Mapping for Z values when projected into the world. private static final float WORLD_NEAR_PLANE = -1.0f; private static final float WORLD_FAR_PLANE = 1.0f; private static final float WORLD_ASPECT_RATIO = (float) WORLD_WIDTH / (float) WORLD_HEIGHT; // The thickness of the walls that bound the world. private static final int MAP_WALL_THICKNESS = 20; // The "map" is the area where ships can move. The map is bounded by the inside edges of // that walls that surround the world. public static final int MAP_WIDTH = WORLD_WIDTH - 2 * MAP_WALL_THICKNESS; public static final int MAP_HEIGHT = WORLD_HEIGHT - 2 * MAP_WALL_THICKNESS; // Define the rectangle that bounds the map. The map and the world share the same coordinate // system centered at 0, 0. public static final int MAP_TOP_COORDINATE = MAP_HEIGHT / 2; public static final int MAP_BOTTOM_COORDINATE = -MAP_HEIGHT / 2; public static final int MAP_LEFT_COORDINATE = -MAP_WIDTH / 2; public static final int MAP_RIGHT_COORDINATE = MAP_WIDTH / 2; // Set to "true" to print info about every controller event. private static final boolean CONTROLLER_DEBUG_PRINT = false; // An arbitrary frame-rate used to compute the "frameDelta" value that is passed to // update and other functions that require a time delta. This frame-rate does not have // any relationship to the actual rate at which the screen refreshes. // See onDrawFrame() and update() for more info on how this value is used. private static final float ANIMATION_FRAMES_PER_SECOND = 60.0f; // The maximum number of controllers supported by this game. private static final int MAX_PLAYERS = 4; // The number of "power-ups" to draw on the map. private static final int MAX_POWERUPS = 2; // The first player to join is red, second is green, etc. private static final Utils.Color PLAYER_COLORS[] = new Utils.Color[] { Utils.Color.RED, Utils.Color.GREEN, Utils.Color.YELLOW, Utils.Color.BLUE }; // The number of points a player must score to win a match. private static final int POINTS_PER_MATCH = 5; // Every particle needs to be checked every frame, even if it is not active. // Therefore, it is best for performance to not over-allocate particles. // Running out of background or explosion particles doesn't affect game play at all, // is likely to go unnoticed by players. private static final int MAX_EXPLOSION_PARTICLES = 2000; private static final int MAX_BACKGROUND_PARTICLES = 400; private static final int MAX_BULLET_PARTICLES = 500; // Singleton instance of the GameState. private static GameState sGameStateInstance = null; // A 4x4 matrix that represents the combined model, view, and projection matrices. private final float[] mMVPMatrix = new float[16]; // The window dimensions in pixels. private int mWindowWidth, mWindowHeight; // One Spaceship per controller. private Spaceship[] mPlayerList; // The animated background particles. private BackgroundParticleSystem mBackgroundParticles; // Manages the shots fired by the ships. private ParticleSystem mShots; // Manages explosion particles. private ParticleSystem mExplosions; // The walls and other obstacles in the world. private WallSegment[] mWallList; // All geometry for the frame goes into a single ShapeBuffer. private ShapeBuffer mShapeBuffer = null; // The list of power ups shown on the map. private PowerUp[] mPowerupList; // The system time (in milliseconds) of the last frame update. private long mLastUpdateTimeMillis; /** * @return The global GameState object. */ public static GameState getInstance() { return sGameStateInstance; } /** * Converts a duration in seconds to a duration in number of elapsed frames. */ public static float secondsToFrameDelta(float seconds) { return seconds * ANIMATION_FRAMES_PER_SECOND; } /** * Converts a duration in milliseconds to a duration in number of elapsed frames. */ public static float millisToFrameDelta(long milliseconds) { return secondsToFrameDelta((float) milliseconds / 1000.0f); } /** * Determines if the given point is within the bounds of the world. * * @param x the x coordinate of the point. * @param y the y coordinate of the point. * @return true of the point is inside the world. */ public static boolean inWorld(float x, float y) { return x >= GameState.WORLD_LEFT_COORDINATE && x <= GameState.WORLD_RIGHT_COORDINATE && y >= GameState.WORLD_BOTTOM_COORDINATE && y <= GameState.WORLD_TOP_COORDINATE; } /** * Set this class as renderer for this GLSurfaceView. * Request Focus and set if focusable in touch mode to * receive the Input from Screen * * @param context The Activity Context. */ public GameState(Context context) { super(context); sGameStateInstance = this; // Request GL ES 2.0 context. setEGLContextClientVersion(2); // Set this as Renderer. this.setRenderer(this); // Request focus. this.requestFocus(); this.setFocusableInTouchMode(true); // Create the lists of players and power-ups. mPlayerList = new Spaceship[MAX_PLAYERS]; for (int i = 0; i < mPlayerList.length; i++) { mPlayerList[i] = new Spaceship(this, i, PLAYER_COLORS[i]); } mPowerupList = new PowerUp[MAX_POWERUPS]; for (int i = 0; i < mPowerupList.length; i++) { mPowerupList[i] = new PowerUp(); } mBackgroundParticles = new BackgroundParticleSystem(MAX_BACKGROUND_PARTICLES); mExplosions = new ParticleSystem(MAX_EXPLOSION_PARTICLES, false); // The true here means we want collision tracking data. mShots = new ParticleSystem(MAX_BULLET_PARTICLES, true); mLastUpdateTimeMillis = System.currentTimeMillis(); InputManager inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); inputManager.registerInputDeviceListener(this, null); buildMap(); } /* ***** Listener Events ***** */ @Override public void onInputDeviceAdded(int arg0) { Utils.logDebug("Device added: " + arg0); } @Override public void onInputDeviceChanged(int arg0) { Utils.logDebug("Device changed: " + arg0); } @Override public void onInputDeviceRemoved(int arg0) { Utils.logDebug("Device removed: " + arg0); // Deactivate a player when their corresponding input device is removed. for (Spaceship player : mPlayerList) { if (player.isActive() && player.getController().getDeviceId() == arg0) { Utils.logDebug("Deactivated player: " + arg0); player.deactivateShip(); } } } public ParticleSystem getShots() { return mShots; } public ParticleSystem getExplosions() { return mExplosions; } public BackgroundParticleSystem getBackgroundParticles() { return mBackgroundParticles; } public Spaceship[] getPlayerList() { return mPlayerList; } public WallSegment[] getWallList() { return mWallList; } /** * The Surface is created/init() */ @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // The ShapeBuffer creates OpenGl resources, so don't create it until after the // primary rendering surface has been created. mShapeBuffer = new ShapeBuffer(); mShapeBuffer.loadResources(); } /** * Here we do our drawing */ @Override public void onDrawFrame(GL10 unused) { // Clear the screen to black. GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // Don't try to draw if the shape buffer failed to initialize. if (!mShapeBuffer.isInitialized()) { return; } long currentTimeMillis = System.currentTimeMillis(); // Compute frame delta. frameDelta = # of "ideal" frames that have occurred since the // last update. "ideal" assumes a constant frame-rate (60 FPS or 16.7 milliseconds per // frame). Since the delta doesn't depend on the "real" frame-rate, the animations always // run at the same wall clock speed, regardless of what the real refresh rate is. // // frameDelta was used instead of a time delta in order to make the values passed // to update easier to understand when debugging the code. For example, a frameDelta // of "1.5" means that one and a half hypothetical frames have passed since the last // update. In wall time this would be 25 milliseconds or 0.025 seconds. float frameDelta = millisToFrameDelta(currentTimeMillis - mLastUpdateTimeMillis); update(frameDelta); draw(); mLastUpdateTimeMillis = currentTimeMillis; } /** * If the surface changes, reset the view size. */ @Override public void onSurfaceChanged(GL10 unused, int width, int height) { // Make sure the window dimensions are never 0. mWindowWidth = Math.max(width, 1); mWindowHeight = Math.max(height, 1); } /** * Indicate that the given player has scored a point. * * Will end the match if the scoring player has enough points to win. * * @param playerId the id of the scoring player. */ public void scorePoint(int playerId) { for (Spaceship player : mPlayerList) { if (player.isActive() && player.getPlayerId() == playerId) { player.changeScore(1); // See if the scoring player has enough points to win the match. if (player.getScore() >= POINTS_PER_MATCH) { endMatch(player); } } } } /** * Handles motion (joystick) input events. * * @param motionEvent The event to handle. * @return true if the event was handled. */ public boolean handleMotionEvent(MotionEvent motionEvent) { Spaceship player = mapInputEventToShip(motionEvent); if (player != null) { player.getController().handleMotionEvent(motionEvent); return true; } return false; } /** * Handles key input events. * * @param keyEvent The event to handle. * @return true if the event was handled. */ public boolean handleKeyEvent(KeyEvent keyEvent) { Spaceship player = mapInputEventToShip(keyEvent); if (player != null) { player.getController().handleKeyEvent(keyEvent); return true; } return false; } /** * Update positions, animations, etc. * * @param frameDelta The amount of time (in "frame units") that has elapsed since the last * call to update(). */ public void update(float frameDelta) { mBackgroundParticles.update(frameDelta); mExplosions.update(frameDelta); mShots.update(frameDelta); for (Spaceship player : mPlayerList) { // Only update the active players. if (player.isActive()) { player.update(frameDelta); } } for (WallSegment wall : mWallList) { wall.update(frameDelta); } for (PowerUp powerUp : mPowerupList) { powerUp.update(frameDelta); } } /** * Draws the world. */ public void draw() { // Each world element adds triangles to the shape buffer. No OpenGl calls are made // until after the whole scene has been added to the shape buffer. mShapeBuffer.clear(); mBackgroundParticles.draw(mShapeBuffer); for (WallSegment wall : mWallList) { wall.draw(mShapeBuffer); } for (PowerUp powerUp : mPowerupList) { powerUp.draw(mShapeBuffer); } mExplosions.draw(mShapeBuffer); for (Spaceship player : mPlayerList) { if (player.isActive()) { player.draw(mShapeBuffer); } } // Draw shots above everything else. mShots.draw(mShapeBuffer); // Prepare for rendering to the screen. updateViewportAndProjection(); // Send the triangles to OpenGl. mShapeBuffer.draw(mMVPMatrix); } /** * Builds the static map, including walls and obstacles. */ private void buildMap() { // The origin of the map coordinate system. final int mapCenterX = 0; final int mapCenterY = 0; final int mapTopWallCenterY = MAP_TOP_COORDINATE + MAP_WALL_THICKNESS / 2; final int mapBottomWallCenterY = MAP_BOTTOM_COORDINATE - MAP_WALL_THICKNESS / 2; final int mapRightWallCenterX = MAP_RIGHT_COORDINATE + MAP_WALL_THICKNESS / 2; final int mapLeftWallCenterX = MAP_LEFT_COORDINATE - MAP_WALL_THICKNESS / 2; final int rectangleShortEdgeLength = 20; final int rectangleLongEdgeLength = 60; final int squareEdgeLength = 20; mWallList = new WallSegment[]{ // Rectangles: // Rectangle touching top edge. new WallSegment( mapCenterX + 40, WORLD_TOP_COORDINATE - rectangleLongEdgeLength / 2, rectangleShortEdgeLength, rectangleLongEdgeLength), // Rectangle touching bottom edge. new WallSegment( mapCenterX - 40, WORLD_BOTTOM_COORDINATE + rectangleLongEdgeLength / 2, rectangleShortEdgeLength, rectangleLongEdgeLength), // Rectangle in center of map. new WallSegment( mapCenterX, mapCenterY, rectangleLongEdgeLength, rectangleShortEdgeLength), // Squares: one in each quadrant of the map. // Square in lower right. new WallSegment( mapCenterX + 80, mapCenterY - 50, squareEdgeLength, squareEdgeLength), // Square in upper left. new WallSegment( mapCenterX - 80, mapCenterY + 50, squareEdgeLength, squareEdgeLength), // Square in upper right. new WallSegment( mapCenterX + 110, mapCenterY + 30, squareEdgeLength, squareEdgeLength), // Square in lower left. new WallSegment( mapCenterX - 110, mapCenterY - 30, squareEdgeLength, squareEdgeLength), // Walls: around the edge of the map. // Top new WallSegment( mapCenterX, mapTopWallCenterY, WORLD_WIDTH, MAP_WALL_THICKNESS), // Bottom new WallSegment( mapCenterX, mapBottomWallCenterY, WORLD_WIDTH, MAP_WALL_THICKNESS), // Right new WallSegment( mapRightWallCenterX, mapCenterY, MAP_WALL_THICKNESS, WORLD_HEIGHT), // Left new WallSegment( mapLeftWallCenterX, mapCenterY, MAP_WALL_THICKNESS, WORLD_HEIGHT), }; } /** * Computes the view projections and sets the OpenGl viewport. */ private void updateViewportAndProjection() { // Assume a square viewport if the width and height haven't been initialized. float viewportAspectRatio = 1.0f; if ((mWindowWidth > 0) && (mWindowHeight > 0)) { viewportAspectRatio = (float) mWindowWidth / (float) mWindowHeight; } float viewportWidth = (float) mWindowWidth; float viewportHeight = (float) mWindowHeight; float viewportOffsetX = 0.0f; float viewportOffsetY = 0.0f; if (WORLD_ASPECT_RATIO > viewportAspectRatio) { // Our window is taller than the ideal aspect ratio needed to accommodate the world // without stretching. // Reduce the viewport height to match the aspect ratio of the world. The world // will fill the whole width of the screen, but have some empty space on the top and // bottom of the screen. viewportHeight = viewportWidth / WORLD_ASPECT_RATIO; // Center the viewport on the screen. viewportOffsetY = ((float) mWindowHeight - viewportHeight) / 2.0f; } else if (viewportAspectRatio > WORLD_ASPECT_RATIO) { // Our window is wider than the ideal aspect ratio needed to accommodate the world // without stretching. // Reduce the viewport width to match the aspect ratio of the world. The world // will fill the whole height of the screen, but have some empty space on the // left and right of the screen. viewportWidth = viewportHeight * WORLD_ASPECT_RATIO; // Center the viewport on the screen. viewportOffsetX = ((float) mWindowWidth - viewportWidth) / 2.0f; } Matrix.orthoM(mMVPMatrix, 0, WORLD_LEFT_COORDINATE, WORLD_RIGHT_COORDINATE, WORLD_BOTTOM_COORDINATE, WORLD_TOP_COORDINATE, WORLD_NEAR_PLANE, WORLD_FAR_PLANE); GLES20.glViewport((int) viewportOffsetX, (int) viewportOffsetY, (int) viewportWidth, (int) viewportHeight); } /** * Finds a player's Spaceship object that corresponds to a given input event. * * Events that do not come from game controllers are ignored. If the event is from a new * controller, the corresponding player's ship is activated. * * @param event The InputEvent to check. * @return a player's ship or null if the event was not from a controller. */ private Spaceship mapInputEventToShip(InputEvent event) { // getControllerNumber() will return "0" for devices that are not game controllers or // joysticks. int controllerNumber = InputDevice.getDevice(event.getDeviceId()).getControllerNumber() - 1; if (CONTROLLER_DEBUG_PRINT) { Utils.logDebug("----------------------------------------------"); Utils.logDebug("Input event: "); Utils.logDebug("Source: " + event.getSource()); Utils.logDebug("isFromSource(gamepad): " + event.isFromSource(InputDevice.SOURCE_GAMEPAD)); Utils.logDebug("isFromSource(joystick): " + event.isFromSource(InputDevice.SOURCE_JOYSTICK)); Utils.logDebug("isFromSource(touch nav): " + event.isFromSource(InputDevice.SOURCE_TOUCH_NAVIGATION)); Utils.logDebug("Controller: " + controllerNumber); Utils.logDebug("----------------------------------------------"); } if (controllerNumber >= 0 && controllerNumber < mPlayerList.length) { // Bind the device to the player's controller. mPlayerList[controllerNumber].getController().setDeviceId(event.getDeviceId()); return mPlayerList[controllerNumber]; } return null; } /** * Prepares the game for a new match. * * @param winningPlayer The winning ship from the last match. */ private void endMatch(Spaceship winningPlayer) { Utils.logDebug("Match over! - Player " + winningPlayer.getPlayerId() + " has " + winningPlayer.getScore() + " points!"); // Reset the score for each ship. for (Spaceship player : mPlayerList) { player.resetAtEndOfMatch(winningPlayer.getPlayerId()); } // Set the background color to the winning player's color until the next // match starts. mBackgroundParticles.flashWinningColor(winningPlayer.getColor()); } }